package org.nanhill.osgi.fileinstall.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.packageadmin.PackageAdmin;

public class DirectoryWatcher extends Thread {
	final static String ALIAS_KEY = "_alias_factory_pid";
	public final static String POLL = "org.nanhill.osgi.fileinstall.poll";
	public final static String DIR = "org.nanhill.osgi.fileinstall.dir";
	public final static String DEBUG = "org.nanhill.osgi.fileinstall.debug";

	private boolean watch = true;

	private long poll;
	private final static long DEF_POLL = 2000L;
	private boolean debug;
	private final static boolean DEF_DEBUG = false;
	private File jardir;
	private final static String DEF_DIR = "bundles";

	private BundleContext context;
	private boolean reported;

	public DirectoryWatcher(Dictionary<String, String> properties,
			BundleContext context) {
		super(properties.toString());
		this.context = context;
		this.poll = getLong(properties, POLL, DEF_POLL);
		this.debug = getBoo(properties, DEBUG, DEF_DEBUG);
		String dir = getString(properties, DIR, DEF_DIR);
		jardir = new File(dir);
		if (!jardir.exists()) {
			log("watch dir " + jardir + " not exist, create it.");
			jardir.mkdirs();
		}
	}

	private long getLong(Dictionary<String, String> properties,
			String property, long df) {
		String value = properties.get(property);
		if (value != null) {
			try {
				return Long.parseLong(value);
			} catch (NumberFormatException e) {
				System.out.println(property + " set, but not a long: " + value);
			}
		}
		return df;
	}

	private boolean getBoo(Dictionary<String, String> properties,
			String property, boolean df) {
		String value = properties.get(property);
		return value == null ? df : Boolean.parseBoolean(value);
	}

	private String getString(Dictionary<String, String> properties,
			String property, String df) {
		String value = properties.get(property);
		return value == null ? df : value;
	}

	public void run() {
		System.out.println(POLL + "  (ms)   " + poll);
		System.out.println(DIR + "            " + jardir.getAbsolutePath());
		System.out.println(DEBUG + "          " + debug);

		// map structure: path name --> modified time
		Map<String, Long> configSnap = new HashMap<String, Long>();
		Map<String, Long> bundSnap = new HashMap<String, Long>();
		while (watch)
			try {
				Map<String, File> bounds = new HashMap<String, File>();
				Map<String, File> configs = new HashMap<String, File>();
				traverse(bounds, configs);
				debug("bound size " + bounds.size(), null);
				doInstalled(bundSnap, bounds);
				doConfigs(configSnap, configs);
				Thread.sleep(poll);
			} catch (InterruptedException e) {
				log("", e);
			} catch (Throwable e) {
				log("In main loop, we have serious trouble", e);
			}
	}

	private void doConfigs(Map<String, Long> old, Map<String, File> newConfigs) {
		try {
			Set oldKeys = new HashSet(old.keySet());
			for (Iterator e = newConfigs.entrySet().iterator(); e.hasNext();) {
				Map.Entry entry = (Map.Entry) e.next();
				String path = (String) entry.getKey();
				File f = (File) entry.getValue();
				if (!oldKeys.contains(path)) {
					// new
					Long l = new Long(f.lastModified());
					if (setConfig(f))
						old.put(path, l);
				} else {
					long lastModified = f.lastModified();
					long oldTime = ((Long) old.get(path)).longValue();
					if (oldTime < lastModified) {
						if (setConfig(f))
							old.put(path, new Long(lastModified));
					}
				}
				oldKeys.remove(path);
			}
			for (Iterator e = oldKeys.iterator(); e.hasNext();) {
				String path = (String) e.next();
				File f = new File(path);
				if (deleteConfig(f))
					old.remove(path);
			}
		} catch (Exception ee) {
			log("Processing config: ", ee);
		}
	}

	private boolean setConfig(File f) throws Exception {
		ConfigurationAdmin cm = (ConfigurationAdmin) Activator.cmTracker
				.getService();
		if (cm == null) {
			log("Can't find a Configuration Manager, configurations do not work");
			if (!reported) {
				reported = true;
			}
			return false;
		}

		Properties p = new Properties();
		InputStream in = new FileInputStream(f);
		p.load(in);
		in.close();
		String pid[] = parsePid(f.getName());
		Hashtable ht = new Hashtable();
		ht.putAll(p);
		if (pid[1] != null)
			ht.put(ALIAS_KEY, pid[1]);
		Configuration config = getConfiguration(pid[0], pid[1]);
		if (config.getBundleLocation() != null)
			config.setBundleLocation(null);
		config.update(ht);
		return true;
	}

	private boolean deleteConfig(File f) throws Exception {
		String pid[] = parsePid(f.getName());
		Configuration config = getConfiguration(pid[0], pid[1]);
		config.delete();
		return true;
	}

	String[] parsePid(String path) {
		String pid = path.substring(0, path.length() - 4);
		int n = pid.indexOf('-');
		if (n > 0) {
			String factoryPid = pid.substring(n + 1);
			pid = pid.substring(0, n);
			return new String[] { pid, factoryPid };
		} else
			return new String[] { pid, null };
	}

	private Configuration getConfiguration(String pid, String factoryPid)
			throws Exception {
		ConfigurationAdmin cm = (ConfigurationAdmin) Activator.cmTracker
				.getService();
		if (factoryPid != null) {
			Configuration configs[] = cm.listConfigurations("(|(" + ALIAS_KEY
					+ "=" + factoryPid + ")(.alias_factory_pid=" + factoryPid
					+ "))");
			if (configs == null || configs.length == 0)
				return cm.createFactoryConfiguration(pid, null);
			else
				return configs[0];
		} else
			return cm.getConfiguration(pid, null);
	}

	private void doInstalled(Map<String, Long> snaps, Map<String, File> watchs) {
		boolean refresh = false;
		debug("watchs " + watchs, null);
		Bundle[] bundles = context.getBundles();
		// update contex bundles
		for (int i = 0; i < bundles.length; i++) {
			Bundle bundle = bundles[i];
			String location = null;
			debug("loc bundle " + bundle.getLocation(), null);
			try {
				location = new File(new URL(bundle.getLocation()).getFile())
						.getAbsolutePath();
			} catch (MalformedURLException e1) {
				continue;
			}
			debug("loc " + location, null);
			File file = (File) watchs.get(location);
			// bundle exists
			if (file != null) {
				debug("remove bundle " + location, null);
				watchs.remove(location);
				Long snapTime = snaps.get(location);
				long fileTime = file.lastModified();
				// bundle updated
				if (snapTime == null || fileTime < snapTime.longValue()) {
					// snap new time
					snaps.put(location, fileTime);
					debug("Start snap " + location, null);
				} else if (fileTime > snapTime.longValue()) {
					refresh = true;
					try {
						InputStream in = new FileInputStream(file);
						bundle.update(in);
						in.close();
						// snap new time
						snaps.put(location, fileTime);
						log("Updated " + location, null);
					} catch (Exception e) {
						log("Failed to update bundle " + location, e);
					}
					if (!isFragment(bundle)) {
						try {
							bundle.start();
							log("started " + location, null);
						} catch (Exception e) {
							log("Fail to start bundle " + location, e);
						}
					}
				} else if (fileTime == snapTime) {
					debug("same bundle " + location, null);
				}
				// bundle file deleted, uninstall
			} else {
				if (location.startsWith(jardir.getAbsolutePath())) {
					refresh = true;
					try {
						bundle.uninstall();
						snaps.remove(location);
						log("Uninstalled " + location, null);
					} catch (Exception e) {
						log("Failed to uninstall bundle " + location, e);
					}
				}
			}
		}

		debug(watchs.toString(), null);

		// new bundle file, install and start
		for (Iterator<File> it = watchs.values().iterator(); it.hasNext();) {
			refresh = true;
			File file = (File) it.next();
			try {
				Bundle bundle = context.installBundle(file.toURL().toString(),
						new FileInputStream(file));
				log("Installed " + file.getAbsolutePath(), null);
				if (!isFragment(bundle))
					bundle.start();
				log("Started " + file.getAbsolutePath(), null);
			} catch (Exception e) {
				log("Failed to install/start bundle: ", e);
			}
		}

		if (refresh)
			refresh();
	}

	private void log(String string, Throwable e) {
		System.out.println("[info]" + string);
		if (e != null && debug)
			e.printStackTrace();
	}

	private void log(String string) {
		log(string, null);
	}

	private void debug(String string, Throwable e) {
		if (debug) {
			System.out.println("[debug]" + string);
			if (e != null) {
				e.printStackTrace();
			}
		}
	}

	private void traverse(Map<String, File> installs, Map<String, File> configs) {
		String[] list = jardir.list();
		for (int i = 0; i < list.length; i++) {
			File file = new File(jardir, list[i]);
			debug("find bundle " + file.getAbsolutePath(), null);
			if (list[i].endsWith(".jar")) {
				installs.put(file.getAbsolutePath(), file);
			} else if (list[i].endsWith(".cfg")) {
				configs.put(file.getAbsolutePath(), file);
			}
		}
	}

	private boolean isFragment(Bundle bundle) {
		PackageAdmin padmin;
		if (Activator.padmin == null)
			return false;

		try {
			padmin = (PackageAdmin) Activator.padmin.waitForService(10000);
			if (padmin != null) {
				return padmin.getBundleType(bundle) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
			}
		} catch (InterruptedException e) {
			// stupid exception
		}
		return false;
	}

	private void refresh() {
		PackageAdmin padmin;
		try {
			padmin = (PackageAdmin) Activator.padmin.waitForService(10000);
			padmin.refreshPackages(null);
			log("refreshed packages ", null);
		} catch (InterruptedException e) {
			log("failed to refresh packages ", e);
		}
	}

	public void close() {
		watch = false;
		interrupt();
		try {
			join(10000);
		} catch (InterruptedException ie) {
			// Ignore
		}
	}

}
